1 module hip.assets.tilemap; 2 public import hip.api.data.tilemap; 3 import hip.config.opts; 4 import hip.assets.image; 5 import hip.asset; 6 7 class HipTilesetImpl : HipAsset, IHipTileset 8 { 9 uint _columns; uint columns() const =>_columns; 10 11 ///Means where the tileset id starts 12 uint _firstGid; uint firstGid() const => _firstGid; 13 14 15 ///"image" in tiled 16 17 string _texturePath; string texturePath() const => _texturePath; 18 ///"imageheight" in tiled 19 uint _textureHeight; uint textureHeight() const => _textureHeight; 20 ///"imagewidth" in tiled 21 uint _textureWidth; uint textureWidth() const => _textureWidth; 22 IHipTexture _texture; IHipTexture texture() => _texture; 23 int _margin; int margin() const => _margin; 24 25 override string name() const => super.name; 26 27 ///Only available when loaded via .tsx 28 string _path; string path() const => _path; 29 int _spacing; int spacing() const => _spacing; 30 uint _tileHeight; uint tileHeight() const => _tileHeight; 31 uint _tileWidth; uint tileWidth() const => _tileWidth; 32 Tile[] _tiles; Tile[] tiles() => _tiles; 33 34 void setTexture(IHipTexture texture) 35 { 36 this._texture = texture; 37 this._textureWidth = texture.getWidth; 38 this._textureHeight = texture.getHeight; 39 } 40 41 // static if(hasTSXSupport) 42 // { 43 // import arsd.dom; 44 45 // static Tileset fromTSX(ubyte[] tsxData, string tsxPath, bool autoLoadTexture = true) 46 // { 47 // string xmlFile = cast(string)tsxData; 48 // auto document = new XmlDocument(xmlFile); 49 // auto tileset = document.querySelector("tileset"); 50 // return Tileset.fromXMLElement(tileset, tsxPath, autoLoadTexture); 51 // } 52 53 54 // static Tileset fromXMLElement(Element tileset, string tsxPath="", bool autoLoadTexture=true) 55 // { 56 // auto image = tileset.querySelector("image"); 57 58 // const uint tileCount = to!uint(tileset.getAttribute("tilecount")); 59 // Tileset ret = new Tileset(tileCount); 60 // ret.path = tsxPath; 61 62 // //Tileset 63 // ret.name = tileset.getAttribute("name"); 64 // ret.tileWidth = to!uint(tileset.getAttribute("tilewidth")); 65 // ret.tileHeight = to!uint(tileset.getAttribute("tileheight")); 66 // ret.columns = to!uint(tileset.getAttribute("columns")); 67 68 // //Image 69 // ret.texturePath = image.getAttribute("source"); 70 // ret.textureWidth = to!uint(image.getAttribute("width")); 71 // ret.textureHeight = to!uint(image.getAttribute("height")); 72 73 // if(autoLoadTexture) 74 // ret.loadTexture(); 75 76 // Element[] tiles = tileset.querySelectorAll("tile"); 77 78 // foreach(t; tiles) 79 // { 80 // Tile tile; 81 // tile.id = to!ushort(t.getAttribute("id")); 82 // Element anim = t.querySelector("animation"); 83 // if(anim !is null) 84 // { 85 // Element[] frames = anim.querySelectorAll("frame"); 86 // tile.animation = new TileAnimationFrame[frames.length]; 87 88 // foreach(f; frames) 89 // { 90 // TileAnimationFrame tFrame; 91 // tFrame.id = to!ushort(f.getAttribute("tileid")); 92 // tFrame.duration = to!int(f.getAttribute("duration")); 93 // } 94 // } 95 // } 96 97 // return ret; 98 // } 99 100 101 // static Tileset fromTSX(string tsxPath, bool autoLoadTexture = true) 102 // { 103 // void[] tsxData; 104 // if(!HipFS.read(tsxPath, tsxData)) 105 // { 106 // import hip.error.handler; 107 // ErrorHandler.showWarningMessage("Could not load TSX ", tsxPath); 108 // return null; 109 // } 110 // return fromTSX(cast(ubyte[])tsxData, tsxPath, autoLoadTexture); 111 // } 112 113 // protected static TileLayer tileLayerFromElement(Element l) 114 // { 115 // import hip.util.string:split; 116 // import hip.util.file:stripLineBreaks; 117 // TileLayer layer = new TileLayer(); 118 // layer.type = TileLayerType.TILE_LAYER; 119 // layer.id = to!ushort(l.getAttribute("id")); 120 // layer.name = l.getAttribute("name"); 121 // layer.width = to!uint(l.getAttribute("width")); 122 // layer.height = to!uint(l.getAttribute("height")); 123 // string[] data = l.querySelector("data").innerText.stripLineBreaks.split(","); 124 // layer.tiles.reserve(data.length); 125 // for(int i = 0; i < data.length;i++) 126 // layer.tiles~=to!ushort(data[i]); 127 128 // return layer; 129 // } 130 131 // protected static TileLayer objectLayerFromElement(Element objgroup) 132 // { 133 // TileLayer layer = new TileLayer(); 134 // layer.type = TileLayerType.OBJECT_LAYER; 135 // layer.id = toDefault!(ushort)(objgroup.getAttribute("id")); 136 // layer.name = objgroup.getAttribute("name"); 137 // Element[] objs = objgroup.querySelectorAll("object"); 138 // foreach(o; objs) 139 // { 140 // TileLayerObject obj; 141 // obj.gid = toDefault!(ushort)(o.getAttribute("gid")); 142 // obj.height = toDefault!(uint)(o.getAttribute("height")); 143 // obj.id = toDefault!(ushort)(o.getAttribute("id")); 144 // obj.name = (o.getAttribute("name")); 145 // obj.rotation= toDefault!(int)(o.getAttribute("rotation")); 146 // obj.type = (o.getAttribute("type")); 147 // obj.visible = toDefault!(bool)(o.getAttribute("visible")); 148 // obj.width = toDefault!(uint)(o.getAttribute("width")); 149 // obj.x = toDefault!(int)(o.getAttribute("x")); 150 // obj.y = toDefault!(int)(o.getAttribute("y")); 151 // Element[] props = o.querySelectorAll("properties"); 152 // foreach(p; props) 153 // { 154 // TileProperty tp; 155 // tp.name = p.getAttribute("name"); 156 // tp.type = p.getAttribute("type"); 157 // tp.value = p.getAttribute("value"); 158 // obj.properties[tp.name] = tp; 159 // } 160 // } 161 // return layer; 162 // } 163 // static Tilemap readTiledTMX(string tiledPath) 164 // { 165 // void[] tmxData; 166 // if(!HipFS.read(tiledPath, tmxData)) 167 // { 168 // import hip.error.handler; 169 // ErrorHandler.showWarningMessage("Could not read Tiled TMX from path ", tiledPath); 170 // return null; 171 // } 172 // return readTiledTMX(cast(ubyte[])tmxData, tiledPath); 173 // } 174 175 // static Tilemap readTiledTMX(ubyte[] tiledData, string tiledPath, bool autoLoadTexture = true) 176 // { 177 // Tilemap ret = new Tilemap(); 178 // string xmlFile = cast(string)tiledData; 179 // auto document = new XmlDocument(xmlFile); 180 // auto map = document.querySelector("map"); 181 // ret.path = tiledPath; 182 183 // ret.tiled_version = map.getAttribute("tiledVersion"); 184 // ret.orientation = map.getAttribute("orientation"); 185 // ret.width = to!uint(map.getAttribute("width")); 186 // ret.height = to!uint(map.getAttribute("height")); 187 // ret.tileWidth = to!uint(map.getAttribute("tilewidth")); 188 // ret.tileHeight = to!uint(map.getAttribute("tileheight")); 189 // ret.isInfinite = (to!uint(map.getAttribute("infinite")) == 1); 190 // ret.renderorder = map.getAttribute("renderorder"); 191 192 // auto tileset = document.querySelectorAll("tileset"); 193 194 // foreach(t; tileset) 195 // { 196 // string tsxPath = t.getAttribute("source"); 197 // Tileset set; 198 // if(tsxPath != null) 199 // set = Tileset.fromTSX(ret.getTSXPath(tsxPath), autoLoadTexture); 200 // else 201 // { 202 // set = Tileset.fromXMLElement(t, ret.getTSXPath("null")); 203 // //Using getTSXPath with any string, as it will be replaced later 204 // //For the texture path 205 // } 206 // set.firstGid = to!uint(t.getAttribute("firstgid")); 207 // ret.tilesets~=set; 208 // } 209 210 // auto layers = document.querySelectorAll("map > layer"); 211 // foreach(l; layers) 212 // { 213 // TileLayer layer = Tilemap.tileLayerFromElement(l); 214 // ret.layersArray~= layer; 215 // ret.layers[layer.name] = layer; 216 // } 217 218 // Element[] objGroups = document.querySelectorAll("map > objectgroup"); 219 // foreach(objGroup; objGroups) 220 // { 221 // TileLayer layer = Tilemap.objectLayerFromElement(objGroup); 222 // ret.layers[layer.name] = layer; 223 // } 224 225 // return ret; 226 // } 227 228 229 // } 230 // else static if(Version.HipTSX) 231 // { 232 // static assert(false, `Please call dub add arsd-official:dom for using TSX parser`); 233 // } 234 235 static HipTilesetImpl read (string path, void delegate(HipTilesetImpl self) onSuccess, void delegate() onError, uint firstGid = 1) 236 { 237 import hip.util.path; 238 switch(path.extension) 239 { 240 case "xml": 241 case "tmx": 242 assert(false, `Please call dub add arsd-official:dom for using TSX parser`); 243 case "tsj": 244 case "json": 245 return HipTilesetImpl.readJSON(path, firstGid, onSuccess, onError); 246 default: 247 assert(false, "Unrecognized extension for file "~path); 248 } 249 } 250 251 import hip.data.json; 252 static HipTilesetImpl readFromMemory (string path, string data, void delegate(HipTilesetImpl) onSuccess, void delegate() onError, uint firstGid = 1) 253 { 254 import hip.util.path; 255 switch(path.extension) 256 { 257 case "xml": 258 case "tmx": 259 assert(false, `Please call dub add arsd-official:dom for using TSX parser`); 260 case "tsj": 261 case "json": 262 HipTilesetImpl ret = new HipTilesetImpl(0); 263 ret._path = path; 264 ret._firstGid = firstGid; 265 ret.loadJSON(parseJSON(data), onSuccess, onError); 266 return ret; 267 default: 268 assert(false, "Unrecognized extension for file "~path); 269 } 270 } 271 272 static HipTilesetImpl readJSON (string path, uint firstGid, void delegate(HipTilesetImpl self) onSuccess, void delegate() onError) 273 { 274 import hip.filesystem.hipfs; 275 import hip.console.log; 276 277 HipTilesetImpl tileset = new HipTilesetImpl(0); 278 tileset._path = path; 279 tileset._firstGid = firstGid; 280 281 HipFS.readText(path).addOnSuccess((in void[] data) 282 { 283 tileset.loadJSON(parseJSON(cast(string)data), onSuccess, onError); 284 }).addOnError((err) 285 { 286 loglnWarn("Could not read file at path ", path," ", err); 287 }); 288 return tileset; 289 } 290 291 public static HipTilesetImpl readJSON (string path, uint firstGid, JSONValue t, void delegate(HipTilesetImpl self) onSuccess, void delegate() onError) 292 { 293 HipTilesetImpl ret = new HipTilesetImpl(0); 294 ret._path = path; 295 ret._firstGid = firstGid; 296 ret.loadJSON(t, onSuccess, onError); 297 return ret; 298 } 299 300 private void loadJSON (JSONValue t, void delegate(HipTilesetImpl self) onSuccess, void delegate() onError) 301 { 302 if(t.hasErrorOccurred) 303 { 304 import hip.error.handler; 305 ErrorHandler.showErrorMessage("JSON Parsing Error on Tilemap", t.toString); 306 return onError(); 307 } 308 _tiles = new Tile[cast(uint)t["tilecount"].integer]; 309 _texturePath = t["image"].str; 310 _textureHeight = cast(uint)t["imageheight"].integer; 311 _textureWidth = cast(uint)t["imagewidth"].integer; 312 _columns = cast(ushort)t["columns"].integer; 313 _margin = cast(int)t["margin"].integer; 314 _name = t["name"].str; 315 _spacing = cast(int)t["spacing"].integer; 316 _tileHeight = cast(uint)t["tileheight"].integer; 317 _tileWidth = cast(uint)t["tilewidth"].integer; 318 319 if("tiles" in t) 320 { 321 foreach (currentTile; t["tiles"].array) 322 { 323 Tile tile; 324 tile.id = cast(ushort)currentTile["id"].integer; 325 326 foreach(prop; currentTile["properties"].array) 327 { 328 TileProperty _p; 329 330 _p.name = prop["name"].str; 331 _p.type = prop["type"].str; 332 _p.value = prop["value"].toString; 333 tile.properties[_p.name] = _p; 334 } 335 tiles[tile.id] = tile; 336 } 337 } 338 onSuccess(this); 339 } 340 341 342 import hip.util.data_structures; 343 static IHipTileset fromSpritesheet(Array2D_GC!IHipTextureRegion regions) 344 { 345 import hip.error.handler; 346 import hip.assets.texture; 347 ErrorHandler.assertExit(regions.getWidth > 0 && regions.getHeight > 0, "Invalid spritesheet"); 348 HipTilesetImpl t = new HipTilesetImpl(regions.getWidth * regions.getHeight); 349 t._name = "Created from Spritesheet"; 350 t._firstGid = 1; 351 t.setTexture(regions[0,0].getTexture); 352 t._tileWidth = regions[0,0].getWidth(); 353 t._tileHeight = regions[0,0].getHeight(); 354 int i = 0; 355 for(int y = 0; y < regions.getHeight; y++) 356 for(int x = 0; x < regions.getWidth; x++) 357 { 358 Tile* tile = &t.tiles[i++]; 359 tile.id = cast(ushort)i; 360 tile.region = regions[x, y]; //TODO: May use clone one day if direct assign doesn't fit 361 // t.region = (cast(HipTextureRegion)regions[x, y]).clone; 362 } 363 364 return t; 365 } 366 import hip.assets.textureatlas; 367 /** 368 * Untested. D's Associative Arrays aren't deterministic, this is subject to bug. 369 */ 370 static IHipTileset fromAtlas(HipTextureAtlas atlas) 371 { 372 HipTilesetImpl t = new HipTilesetImpl(cast(uint)atlas.frames.length); 373 t._firstGid = 1; 374 t._name = "Tileset from Atlas: "~atlas.name; 375 t.setTexture(atlas.texture); 376 int i = 0; 377 foreach(atlasFrame; atlas) 378 { 379 Tile* tile = &t.tiles[i++]; 380 if(!t.tileWidth) 381 { 382 t._tileWidth = atlasFrame.region.getWidth; 383 t._tileHeight = atlasFrame.region.getHeight; 384 } 385 //TODO: May use clone one day if direct assign doesn't fit. 386 tile.region = atlasFrame.region; 387 tile.id = cast(ushort)i; 388 } 389 return t; 390 } 391 392 this(uint tileCount) 393 { 394 super("HipTileset"); 395 _tiles = new Tile[tileCount]; 396 _typeID = assetTypeID!HipTilesetImpl; 397 } 398 IImage textureImage; 399 400 IImage loadImage(void delegate(IImage self) onSuccess, void delegate() onFailure) 401 { 402 import hip.error.handler; 403 import hip.filesystem.hipfs; 404 import hip.util.path; 405 if(textureImage is null) 406 { 407 ErrorHandler.assertExit(texturePath != "", "No texture path for loading tilemap texture"); 408 string imagePath = replaceFileName(path, texturePath); 409 HipFS.read(imagePath).addOnSuccess((in ubyte[] imgData) 410 { 411 textureImage = new Image(imagePath, cast(ubyte[])imgData, onSuccess, onFailure); 412 }).addOnError((string err) 413 { 414 ErrorHandler.showErrorMessage("Error loading image required by Tileset", imagePath); 415 onFailure(); 416 }); 417 } 418 return textureImage; 419 } 420 421 bool loadTexture() 422 { 423 import hip.error.handler; 424 import hip.assets.texture; 425 if(textureImage is null) 426 { 427 loadImage((_){loadTexture();}, (){}); 428 return false; 429 } 430 _texture = new HipTexture(textureImage); 431 int i = 0; 432 for(int y = margin; y < textureHeight; y+= (tileHeight+spacing)) 433 for(int x = margin, currCol = 0 ; currCol < columns; currCol++, x+= (tileWidth+spacing)) 434 { 435 Tile* t = &tiles[i]; 436 t.region = new HipTextureRegion(texture, x, y, x+tileWidth, y+tileHeight); 437 i++; 438 } 439 440 return texture !is null && texture.hasSuccessfullyLoaded(); 441 } 442 443 override void onFinishLoading(){} 444 override void onDispose(){} 445 bool isReady(){return _texture !is null;} 446 } 447 448 449 class HipTilemap : HipAsset, IHipTilemap 450 { 451 452 int _x, _y; 453 HipColor _color = HipColor.white; 454 float _scaleX = 1.0, _scaleY = 1.0; 455 float _rotation = 0; 456 457 string _path; 458 uint _width, _height; 459 bool _isInfinite; 460 HipTileLayer[string] _layers; 461 string _orientation; 462 string _renderOrder; 463 string _tiledVersion; 464 uint _tileWidth, _tileHeight; 465 466 this(uint width = 0, uint height = 0, uint tileWidth = 0, uint tileHeight = 0) 467 { 468 super("HipTilemap"); 469 _typeID = assetTypeID!HipTilemap; 470 this._width = width; 471 this._height = height; 472 this._tileWidth = tileWidth; 473 this._tileHeight = tileHeight; 474 } 475 476 ref int x() => _x; 477 ref int y() => _y; 478 ref HipColor color() => _color; 479 ref float scaleX() => _scaleX; 480 ref float scaleY() => _scaleY; 481 float scale() => _scaleX; 482 float scale(float sc) => _scaleX = _scaleY = sc; 483 ref float rotation() => _rotation; 484 485 ///Used for rendering order 486 string path() const => _path; 487 uint width() const => _width; 488 uint height() const => _height; 489 bool isInfinite() const => _isInfinite; 490 ref HipTileLayer[string] layers() => _layers; 491 string orientation() const => _orientation; 492 string renderorder() const => _renderOrder; 493 string tiled_version() const => _tiledVersion; 494 uint tileHeight() const => _tileHeight; 495 uint tileWidth() const => _tileWidth; 496 497 void setTileSize(uint tileWidth, uint tileHeight) 498 { 499 _tileWidth = tileWidth; 500 _tileHeight = tileHeight; 501 } 502 503 void addTileset(IHipTileset tileset){tilesets~= cast(HipTilesetImpl)tileset;} 504 505 protected HipTileLayer[] layersArray; 506 HipTilesetImpl[] tilesets; 507 this() 508 { 509 super("HipTilemap"); 510 _typeID = assetTypeID!HipTilemap; 511 } 512 513 IHipTileset getTilesetForID(ushort id) 514 { 515 if(tilesets.length == 0) 516 return null; 517 for(int i = 0; i < cast(int)tilesets.length-1; i++) 518 { 519 if(id >= tilesets[i].firstGid && id < tilesets[i+1].firstGid) 520 return tilesets[i]; 521 } 522 return tilesets[$-1]; 523 } 524 525 526 string getTSXPath(string tsxName) 527 { 528 import hip.util.path : replaceFileName; 529 return replaceFileName(path, tsxName); 530 } 531 532 static HipTilemap readTiledJSON (string mapPath, ubyte[] tiledData, void delegate(HipTilemap) onSuccess, void delegate() onError) 533 { 534 import hip.data.json; 535 HipTilemap ret = new HipTilemap(); 536 ret._path = mapPath; 537 JSONValue json = parseJSON(cast(string)(tiledData)); 538 ret._height = cast(uint)json["height"].integer; 539 ret._isInfinite = json["infinite"].boolean; 540 ret._width = cast(uint)json["width"].integer; 541 ret._orientation= json["orientation"].str; 542 ret._renderOrder= json["renderorder"].str; 543 ret._tileHeight = cast(uint)json["tileheight"].integer; 544 ret._tileWidth = cast(uint)json["tilewidth"].integer; 545 546 foreach(l; json["layers"].array) 547 { 548 HipTileLayer layer = new HipTileLayer(ret); 549 550 //Check first the layer type. 551 layer.type = l["type"].str; 552 layer.id = cast(ushort)l["id"].integer; 553 layer.name = l["name"].str; 554 layer.opacity = l["opacity"].integer; 555 layer.visible = l["visible"].boolean; 556 layer.x = cast(int) l["x"].integer; 557 layer.y = cast(int) l["y"].integer; 558 layer.columns = cast(int) l["width"].integer; 559 layer.rows = cast(int) l["height"].integer; 560 if(layer.type == TileLayerType.OBJECT_LAYER) 561 { 562 foreach(o; l["objects"].array) 563 { 564 TileLayerObject obj; 565 obj.gid = cast(ushort)o["gid"].integer; 566 obj.height = cast(uint) o["height"].integer; 567 obj.id = cast(ushort)o["id"].integer; 568 obj.name = o["name"].str; 569 obj.rotation= cast(int) o["rotation"].integer; 570 obj.type = o["type"].str; 571 obj.visible = o["visible"].boolean; 572 obj.width = cast(uint) o["width"].integer; 573 obj.x = cast(int) o["x"].integer; 574 obj.y = cast(int) o["y"].integer; 575 576 const(JSONValue)* v = ("properties" in o); 577 if(v != null) 578 { 579 foreach(p; v.array) //Properties 580 { 581 TileProperty tp; 582 tp.name = p["name"].str; 583 tp.type = p["type"].str; 584 tp.value = p["value"].toString; 585 586 obj.properties[tp.name] = tp; 587 } 588 } 589 } 590 } 591 else if(layer.type == TileLayerType.TILE_LAYER) 592 { 593 auto layerData = l["data"].array; 594 layer.height = cast(uint) l["height"].integer; 595 layer.width = cast(uint) l["width"].integer; 596 layer.tiles.reserve(layerData.length); 597 foreach(d; layerData) 598 layer.tiles~= cast(ushort)d.integer; 599 } 600 601 const(JSONValue)* layerProp = ("properties" in l); 602 if(layerProp != null) 603 { 604 foreach(p; layerProp.array) 605 { 606 TileProperty tp; 607 tp.name = p["name"].str; 608 tp.type = p["type"].str; 609 tp.value = p["value"].toString; 610 layer.properties[tp.name] = tp; 611 } 612 } 613 ret.layersArray~=layer; 614 ret._layers[layer.name] = layer; 615 } 616 617 size_t maxTilesets = json["tilesets"].array.length; 618 uint loadedCount = 0; 619 auto onTilesetLoad = delegate(HipTilesetImpl tileset) 620 { 621 if(++loadedCount == maxTilesets) 622 onSuccess(ret); 623 }; 624 625 626 foreach(t; json["tilesets"].array) 627 { 628 const(JSONValue)* source = ("source" in t); 629 uint firstGid = cast(ushort)t["firstgid"].integer; 630 HipTilesetImpl tileset; 631 632 if(source !is null) 633 { 634 import hip.util.path; 635 import hip.console.log; 636 loglnWarn("Reading from source "); 637 tileset = HipTilesetImpl.read(joinPath(dirName(mapPath), source.str), onTilesetLoad, onError, firstGid); 638 } 639 else 640 tileset = HipTilesetImpl.readJSON("null", firstGid, t, onTilesetLoad, onError); 641 ret.tilesets~= tileset; 642 } 643 644 return ret; 645 } 646 static void readTiledJSON (string tiledPath, void delegate(HipTilemap) onSuccess, void delegate() onError) 647 { 648 import hip.filesystem.hipfs; 649 HipFS.read(tiledPath).addOnSuccess((in ubyte[] data) 650 { 651 HipTilemap.readTiledJSON(tiledPath, cast(ubyte[])data, onSuccess, onError); 652 }).addOnError((err) 653 { 654 import hip.error.handler; 655 ErrorHandler.showWarningMessage("Could not read Tiled TMX from path ", tiledPath); 656 onError(); 657 }); 658 } 659 660 661 ///Those arguments are actually synchronous on all platforms. This is to simulate JS API. 662 void loadImages(void delegate() onSuccess, void delegate() onFailure) 663 { 664 int counter = 0; 665 auto onSuccessInternal = delegate(IImage _) 666 { 667 if(++counter == tilesets.length) 668 onSuccess(); 669 }; 670 foreach(HipTilesetImpl tileset; tilesets) 671 tileset.loadImage(onSuccessInternal, onFailure); 672 } 673 674 bool loadTextures() 675 { 676 foreach(HipTilesetImpl tileset; tilesets) 677 if(!tileset.loadTexture()) 678 return false; 679 return true; 680 } 681 682 override void onFinishLoading(){} 683 override void onDispose(){} 684 bool isReady(){return true;} 685 686 687 }